/**
 * Copyright (C) 2013 Daniel Gee.
 * See LICENSE.txt for more details.
 */
package com.rl.dungeon.fov;

import com.rl.dungeon.Location;

/**
 * Allows the pre-computation of all FOVs for a given map. If your map's terrain isn't going to
 * change, or isn't going to change often, then you can use this instead of regular FOV calls.
 * Checking FOV is faster, but at the expense of a little memory and a little setup time.
 * 
 * @author Daniel Gee
 */
public class PreComputedFOVMap
{
	/**
	 * A dummy inner class for the VisitEffect, since java7 doesn't support lambdas.
	 */
	class LOSSetter implements VisitEffect
	{
		/**
		 * the source of this visit effect.
		 */
		final Location here;

		/**
		 * Makes a new one
		 * 
		 * @param loc
		 *            the source.
		 */
		LOSSetter(Location loc)
		{
			this.here = loc;
		}

		/**
		 * @see com.rl.dungeon.fov.VisitEffect#performEffect(com.rl.dungeon.Location)
		 */
		@Override
		public void performEffect(Location location)
		{
			setLos(here, location, true);
		}
	}

	/**
	 * The chart that tracks if LOS exists between two locations or not.
	 */
	private boolean[][][][] chart;

	/**
	 * The upper bound of this FOV map.
	 */
	private final Location edge;

	/**
	 * Thw lower bound of this FOV map.
	 */
	private final Location origin;

	/**
	 * Makes a new {@link PreComputedFOVMap} covering the area from the <tt>origin</tt> out to the
	 * <tt>edge</tt> according to the <tt>check</tt> provided.
	 * 
	 * @param origin
	 *            the lower bound.
	 * @param edge
	 *            the upper bound.
	 * @param check
	 *            the FOV check for this map.
	 * @throws IllegalArgumentException
	 *             if the <tt>origin</tt> and <tt>edge</tt> don't share a z coordinate, or if either
	 *             of the edge's x,y coordinates aren't greater than the origin's coordinate on the
	 *             same axis.
	 */
	public PreComputedFOVMap(Location origin, Location edge, VisionCheck check)
	{
		if (origin.z != edge.z)
		{
			throw new IllegalArgumentException("Origin and Edge must be on the same level, got: "
					+ origin.z + " and " + edge.z);
		}
		if (edge.x <= origin.x || edge.y <= origin.y)
		{
			throw new IllegalArgumentException(
					"The Edge's x and y coordinates must both be greater than the Origin's, got: Or:"
							+ origin + ", Ed:" + edge);
		}
		this.origin = origin;
		this.edge = edge;
		int xSpan = edge.x - origin.x;
		int ySpan = edge.y - origin.y;
		this.chart = new boolean[xSpan][ySpan][xSpan][ySpan];

		for (int x = this.origin.x; x < this.edge.x; x++)
		{
			for (int y = this.origin.y; y < this.edge.y; y++)
			{
				final Location translatedOrigin = origin.translate(x, y);
				PrecisePermissiveFOV.computeFOV(translatedOrigin, edge, Math.max(xSpan, ySpan),
						new LOSSetter(translatedOrigin), check);
			}
		}
	}

	/**
	 * Returns if Line of Sight exists between the two locations provided.
	 * 
	 * @param a
	 *            one location.
	 * @param b
	 *            another location.
	 * @return if Line of Sight exists between the locations.
	 */
	public boolean hasLOSBetween(Location a, Location b)
	{
		if (!withinBounds(a) || !withinBounds(b))
		{
			return false;
		}
		final Location translatedA = a.translate(this.origin.x, this.origin.y);
		final Location translatedB = b.translate(this.origin.x, this.origin.y);
		return this.chart[translatedA.x][translatedA.y][translatedB.x][translatedB.y];
	}

	/**
	 * Assigns if LOS exists between the two locations or not. Assumes that LOS is reflective and
	 * assigns accordingly.
	 * 
	 * @param a
	 *            one location
	 * @param b
	 *            another location
	 * @param value
	 *            if LOS exists between the locations or not.
	 */
	protected void setLos(Location a, Location b, boolean value)
	{
		this.chart[a.x][a.y][b.x][b.y] = value;
		this.chart[b.x][b.y][a.x][a.y] = value;
	}

	/**
	 * Checks to see if the location specified is within bounds of this {@link PreComputedFOVMap} or
	 * not.
	 * 
	 * @param location
	 *            the location in question.
	 * @return <tt>true</tt> if the location is in bounds or not.
	 */
	private boolean withinBounds(Location location)
	{
		return location.x >= origin.x && location.x <= edge.x && location.y >= origin.y
				&& location.y <= edge.y;
	}
}
